// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cli

import (
	"bytes"
	"go/ast"
	"go/parser"
	"go/token"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestHasLensAnnotation(t *testing.T) {
	tests := []struct {
		name     string
		comment  string
		expected bool
	}{
		{
			name:     "has annotation",
			comment:  "// fp-go:Lens",
			expected: true,
		},
		{
			name:     "has annotation with other text",
			comment:  "// This is a struct with fp-go:Lens annotation",
			expected: true,
		},
		{
			name:     "no annotation",
			comment:  "// This is just a regular comment",
			expected: false,
		},
		{
			name:     "nil comment",
			comment:  "",
			expected: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var doc *ast.CommentGroup
			if tt.comment != "" {
				doc = &ast.CommentGroup{
					List: []*ast.Comment{
						{Text: tt.comment},
					},
				}
			}
			result := hasLensAnnotation(doc)
			assert.Equal(t, tt.expected, result)
		})
	}
}

func TestGetTypeName(t *testing.T) {
	tests := []struct {
		name     string
		code     string
		expected string
	}{
		{
			name:     "simple type",
			code:     "type T struct { F string }",
			expected: "string",
		},
		{
			name:     "pointer type",
			code:     "type T struct { F *string }",
			expected: "*string",
		},
		{
			name:     "slice type",
			code:     "type T struct { F []int }",
			expected: "[]int",
		},
		{
			name:     "map type",
			code:     "type T struct { F map[string]int }",
			expected: "map[string]int",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			fset := token.NewFileSet()
			file, err := parser.ParseFile(fset, "", "package test\n"+tt.code, 0)
			require.NoError(t, err)

			var fieldType ast.Expr
			ast.Inspect(file, func(n ast.Node) bool {
				if field, ok := n.(*ast.Field); ok && len(field.Names) > 0 {
					fieldType = field.Type
					return false
				}
				return true
			})

			require.NotNil(t, fieldType)
			result := getTypeName(fieldType)
			assert.Equal(t, tt.expected, result)
		})
	}
}

func TestIsPointerType(t *testing.T) {
	tests := []struct {
		name     string
		code     string
		expected bool
	}{
		{
			name:     "pointer type",
			code:     "type T struct { F *string }",
			expected: true,
		},
		{
			name:     "non-pointer type",
			code:     "type T struct { F string }",
			expected: false,
		},
		{
			name:     "slice type",
			code:     "type T struct { F []string }",
			expected: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			fset := token.NewFileSet()
			file, err := parser.ParseFile(fset, "", "package test\n"+tt.code, 0)
			require.NoError(t, err)

			var fieldType ast.Expr
			ast.Inspect(file, func(n ast.Node) bool {
				if field, ok := n.(*ast.Field); ok && len(field.Names) > 0 {
					fieldType = field.Type
					return false
				}
				return true
			})

			require.NotNil(t, fieldType)
			result := isPointerType(fieldType)
			assert.Equal(t, tt.expected, result)
		})
	}
}

func TestIsComparableType(t *testing.T) {
	tests := []struct {
		name     string
		code     string
		expected bool
	}{
		{
			name:     "basic type - string",
			code:     "type T struct { F string }",
			expected: true,
		},
		{
			name:     "basic type - int",
			code:     "type T struct { F int }",
			expected: true,
		},
		{
			name:     "basic type - bool",
			code:     "type T struct { F bool }",
			expected: true,
		},
		{
			name:     "pointer type",
			code:     "type T struct { F *string }",
			expected: true,
		},
		{
			name:     "slice type - not comparable",
			code:     "type T struct { F []string }",
			expected: false,
		},
		{
			name:     "map type - not comparable",
			code:     "type T struct { F map[string]int }",
			expected: false,
		},
		{
			name:     "array type - comparable if element is",
			code:     "type T struct { F [5]int }",
			expected: true,
		},
		{
			name:     "interface type",
			code:     "type T struct { F interface{} }",
			expected: true,
		},
		{
			name:     "channel type",
			code:     "type T struct { F chan int }",
			expected: true,
		},
		{
			name:     "function type - not comparable",
			code:     "type T struct { F func() }",
			expected: false,
		},
		{
			name:     "struct literal - conservatively not comparable",
			code:     "type T struct { F struct{ X int } }",
			expected: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			fset := token.NewFileSet()
			file, err := parser.ParseFile(fset, "", "package test\n"+tt.code, 0)
			require.NoError(t, err)

			var fieldType ast.Expr
			ast.Inspect(file, func(n ast.Node) bool {
				if field, ok := n.(*ast.Field); ok && len(field.Names) > 0 {
					fieldType = field.Type
					return false
				}
				return true
			})

			require.NotNil(t, fieldType)
			result := isComparableType(fieldType, map[string]string{})
			assert.Equal(t, tt.expected, result)
		})
	}
}

func TestHasOmitEmpty(t *testing.T) {
	tests := []struct {
		name     string
		tag      string
		expected bool
	}{
		{
			name:     "has omitempty",
			tag:      "`json:\"field,omitempty\"`",
			expected: true,
		},
		{
			name:     "has omitempty with other options",
			tag:      "`json:\"field,omitempty,string\"`",
			expected: true,
		},
		{
			name:     "no omitempty",
			tag:      "`json:\"field\"`",
			expected: false,
		},
		{
			name:     "no tag",
			tag:      "",
			expected: false,
		},
		{
			name:     "different tag",
			tag:      "`xml:\"field\"`",
			expected: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var tag *ast.BasicLit
			if tt.tag != "" {
				tag = &ast.BasicLit{
					Value: tt.tag,
				}
			}
			result := hasOmitEmpty(tag)
			assert.Equal(t, tt.expected, result)
		})
	}
}

func TestParseFile(t *testing.T) {
	// Create a temporary test file
	tmpDir := t.TempDir()
	testFile := filepath.Join(tmpDir, "test.go")

	testCode := `package testpkg

// fp-go:Lens
type Person struct {
	Name  string
	Age   int
	Phone *string
}

// fp-go:Lens
type Address struct {
	Street string
	City   string
}

// Not annotated
type Other struct {
	Field string
}
`

	err := os.WriteFile(testFile, []byte(testCode), 0644)
	require.NoError(t, err)

	// Parse the file
	structs, pkg, err := parseFile(testFile)
	require.NoError(t, err)

	// Verify results
	assert.Equal(t, "testpkg", pkg)
	assert.Len(t, structs, 2)

	// Check Person struct
	person := structs[0]
	assert.Equal(t, "Person", person.Name)
	assert.Len(t, person.Fields, 3)

	assert.Equal(t, "Name", person.Fields[0].Name)
	assert.Equal(t, "string", person.Fields[0].TypeName)
	assert.False(t, person.Fields[0].IsOptional)

	assert.Equal(t, "Age", person.Fields[1].Name)
	assert.Equal(t, "int", person.Fields[1].TypeName)
	assert.False(t, person.Fields[1].IsOptional)

	assert.Equal(t, "Phone", person.Fields[2].Name)
	assert.Equal(t, "*string", person.Fields[2].TypeName)
	assert.True(t, person.Fields[2].IsOptional)

	// Check Address struct
	address := structs[1]
	assert.Equal(t, "Address", address.Name)
	assert.Len(t, address.Fields, 2)

	assert.Equal(t, "Street", address.Fields[0].Name)
	assert.Equal(t, "City", address.Fields[1].Name)
}

func TestParseFileWithOmitEmpty(t *testing.T) {
	// Create a temporary test file
	tmpDir := t.TempDir()
	testFile := filepath.Join(tmpDir, "test.go")

	testCode := `package testpkg

// fp-go:Lens
type Config struct {
	Name     string
	Value    string  ` + "`json:\"value,omitempty\"`" + `
	Count    int     ` + "`json:\",omitempty\"`" + `
	Optional *string ` + "`json:\"optional,omitempty\"`" + `
	Required int     ` + "`json:\"required\"`" + `
}
`

	err := os.WriteFile(testFile, []byte(testCode), 0644)
	require.NoError(t, err)

	// Parse the file
	structs, pkg, err := parseFile(testFile)
	require.NoError(t, err)

	// Verify results
	assert.Equal(t, "testpkg", pkg)
	assert.Len(t, structs, 1)

	// Check Config struct
	config := structs[0]
	assert.Equal(t, "Config", config.Name)
	assert.Len(t, config.Fields, 5)

	// Name - no tag, not optional
	assert.Equal(t, "Name", config.Fields[0].Name)
	assert.Equal(t, "string", config.Fields[0].TypeName)
	assert.False(t, config.Fields[0].IsOptional)

	// Value - has omitempty, should be optional
	assert.Equal(t, "Value", config.Fields[1].Name)
	assert.Equal(t, "string", config.Fields[1].TypeName)
	assert.True(t, config.Fields[1].IsOptional, "Value field with omitempty should be optional")

	// Count - has omitempty (no field name in tag), should be optional
	assert.Equal(t, "Count", config.Fields[2].Name)
	assert.Equal(t, "int", config.Fields[2].TypeName)
	assert.True(t, config.Fields[2].IsOptional, "Count field with omitempty should be optional")

	// Optional - pointer with omitempty, should be optional
	assert.Equal(t, "Optional", config.Fields[3].Name)
	assert.Equal(t, "*string", config.Fields[3].TypeName)
	assert.True(t, config.Fields[3].IsOptional)

	// Required - has json tag but no omitempty, not optional
	assert.Equal(t, "Required", config.Fields[4].Name)
	assert.Equal(t, "int", config.Fields[4].TypeName)
	assert.False(t, config.Fields[4].IsOptional, "Required field without omitempty should not be optional")
}

func TestParseFileWithComparableTypes(t *testing.T) {
	// Create a temporary test file
	tmpDir := t.TempDir()
	testFile := filepath.Join(tmpDir, "test.go")

	testCode := `package testpkg

// fp-go:Lens
type TypeTest struct {
	Name      string
	Age       int
	Pointer   *string
	Slice     []string
	Map       map[string]int
	Channel   chan int
}
`

	err := os.WriteFile(testFile, []byte(testCode), 0644)
	require.NoError(t, err)

	// Parse the file
	structs, pkg, err := parseFile(testFile)
	require.NoError(t, err)

	// Verify results
	assert.Equal(t, "testpkg", pkg)
	assert.Len(t, structs, 1)

	// Check TypeTest struct
	typeTest := structs[0]
	assert.Equal(t, "TypeTest", typeTest.Name)
	assert.Len(t, typeTest.Fields, 6)

	// Name - string is comparable
	assert.Equal(t, "Name", typeTest.Fields[0].Name)
	assert.Equal(t, "string", typeTest.Fields[0].TypeName)
	assert.False(t, typeTest.Fields[0].IsOptional)
	assert.True(t, typeTest.Fields[0].IsComparable, "string should be comparable")

	// Age - int is comparable
	assert.Equal(t, "Age", typeTest.Fields[1].Name)
	assert.Equal(t, "int", typeTest.Fields[1].TypeName)
	assert.False(t, typeTest.Fields[1].IsOptional)
	assert.True(t, typeTest.Fields[1].IsComparable, "int should be comparable")

	// Pointer - pointer is optional, IsComparable not checked for optional fields
	assert.Equal(t, "Pointer", typeTest.Fields[2].Name)
	assert.Equal(t, "*string", typeTest.Fields[2].TypeName)
	assert.True(t, typeTest.Fields[2].IsOptional)

	// Slice - not comparable
	assert.Equal(t, "Slice", typeTest.Fields[3].Name)
	assert.Equal(t, "[]string", typeTest.Fields[3].TypeName)
	assert.False(t, typeTest.Fields[3].IsOptional)
	assert.False(t, typeTest.Fields[3].IsComparable, "slice should not be comparable")

	// Map - not comparable
	assert.Equal(t, "Map", typeTest.Fields[4].Name)
	assert.Equal(t, "map[string]int", typeTest.Fields[4].TypeName)
	assert.False(t, typeTest.Fields[4].IsOptional)
	assert.False(t, typeTest.Fields[4].IsComparable, "map should not be comparable")

	// Channel - comparable (note: getTypeName returns "any" for channel types, but isComparableType correctly identifies them)
	assert.Equal(t, "Channel", typeTest.Fields[5].Name)
	assert.Equal(t, "any", typeTest.Fields[5].TypeName) // getTypeName doesn't handle chan types specifically
	assert.False(t, typeTest.Fields[5].IsOptional)
	assert.True(t, typeTest.Fields[5].IsComparable, "channel should be comparable")
}

func TestLensRefTemplatesWithComparable(t *testing.T) {
	s := structInfo{
		Name: "TestStruct",
		Fields: []fieldInfo{
			{Name: "Name", TypeName: "string", IsOptional: false, IsComparable: true},
			{Name: "Age", TypeName: "int", IsOptional: false, IsComparable: true},
			{Name: "Data", TypeName: "[]byte", IsOptional: false, IsComparable: false},
			{Name: "Pointer", TypeName: "*string", IsOptional: true, IsComparable: false},
		},
	}

	// Test constructor template for RefLenses
	var constructorBuf bytes.Buffer
	err := constructorTmpl.Execute(&constructorBuf, s)
	require.NoError(t, err)

	constructorStr := constructorBuf.String()

	// Check that MakeLensStrict is used for comparable types in RefLenses
	assert.Contains(t, constructorStr, "func MakeTestStructRefLenses() TestStructRefLenses")

	// Name field - comparable, should use MakeLensStrict
	assert.Contains(t, constructorStr, "lensName := L.MakeLensStrict(",
		"comparable field Name should use MakeLensStrict in RefLenses")

	// Age field - comparable, should use MakeLensStrict
	assert.Contains(t, constructorStr, "lensAge := L.MakeLensStrict(",
		"comparable field Age should use MakeLensStrict in RefLenses")

	// Data field - not comparable, should use MakeLensRef
	assert.Contains(t, constructorStr, "lensData := L.MakeLensRef(",
		"non-comparable field Data should use MakeLensRef in RefLenses")

}

func TestGenerateLensHelpersWithComparable(t *testing.T) {
	// Create a temporary directory with test files
	tmpDir := t.TempDir()

	testCode := `package testpkg

// fp-go:Lens
type TestStruct struct {
	Name  string
	Count int
	Data  []byte
}
`

	testFile := filepath.Join(tmpDir, "test.go")
	err := os.WriteFile(testFile, []byte(testCode), 0644)
	require.NoError(t, err)

	// Generate lens code
	outputFile := "gen.go"
	err = generateLensHelpers(tmpDir, outputFile, false)
	require.NoError(t, err)

	// Verify the generated file exists
	genPath := filepath.Join(tmpDir, outputFile)
	_, err = os.Stat(genPath)
	require.NoError(t, err)

	// Read and verify the generated content
	content, err := os.ReadFile(genPath)
	require.NoError(t, err)

	contentStr := string(content)

	// Check for expected content in RefLenses
	assert.Contains(t, contentStr, "MakeTestStructRefLenses")

	// Name and Count are comparable, should use MakeLensStrict
	assert.Contains(t, contentStr, "L.MakeLensStrict",
		"comparable fields should use MakeLensStrict in RefLenses")

	// Data is not comparable (slice), should use MakeLensRef
	assert.Contains(t, contentStr, "L.MakeLensRef",
		"non-comparable fields should use MakeLensRef in RefLenses")

	// Verify the pattern appears for Name field (comparable)
	namePattern := "lensName := L.MakeLensStrict("
	assert.Contains(t, contentStr, namePattern,
		"Name field should use MakeLensStrict")

	// Verify the pattern appears for Data field (not comparable)
	dataPattern := "lensData := L.MakeLensRef("
	assert.Contains(t, contentStr, dataPattern,
		"Data field should use MakeLensRef")
}

func TestGenerateLensHelpers(t *testing.T) {
	// Create a temporary directory with test files
	tmpDir := t.TempDir()

	testCode := `package testpkg

// fp-go:Lens
type TestStruct struct {
	Name  string
	Value *int
}
`

	testFile := filepath.Join(tmpDir, "test.go")
	err := os.WriteFile(testFile, []byte(testCode), 0644)
	require.NoError(t, err)

	// Generate lens code
	outputFile := "gen.go"
	err = generateLensHelpers(tmpDir, outputFile, false)
	require.NoError(t, err)

	// Verify the generated file exists
	genPath := filepath.Join(tmpDir, outputFile)
	_, err = os.Stat(genPath)
	require.NoError(t, err)

	// Read and verify the generated content
	content, err := os.ReadFile(genPath)
	require.NoError(t, err)

	contentStr := string(content)

	// Check for expected content
	assert.Contains(t, contentStr, "package testpkg")
	assert.Contains(t, contentStr, "Code generated by go generate")
	assert.Contains(t, contentStr, "TestStructLenses")
	assert.Contains(t, contentStr, "MakeTestStructLenses")
	assert.Contains(t, contentStr, "L.Lens[TestStruct, string]")
	assert.Contains(t, contentStr, "LO.LensO[TestStruct, *int]")
	assert.Contains(t, contentStr, "IO.FromZero")
}

func TestGenerateLensHelpersNoAnnotations(t *testing.T) {
	// Create a temporary directory with test files
	tmpDir := t.TempDir()

	testCode := `package testpkg

// No annotation
type TestStruct struct {
	Name string
}
`

	testFile := filepath.Join(tmpDir, "test.go")
	err := os.WriteFile(testFile, []byte(testCode), 0644)
	require.NoError(t, err)

	// Generate lens code (should not create file)
	outputFile := "gen.go"
	err = generateLensHelpers(tmpDir, outputFile, false)
	require.NoError(t, err)

	// Verify the generated file does not exist
	genPath := filepath.Join(tmpDir, outputFile)
	_, err = os.Stat(genPath)
	assert.True(t, os.IsNotExist(err))
}

func TestLensTemplates(t *testing.T) {
	s := structInfo{
		Name: "TestStruct",
		Fields: []fieldInfo{
			{Name: "Name", TypeName: "string", IsOptional: false, IsComparable: true},
			{Name: "Value", TypeName: "*int", IsOptional: true, IsComparable: true},
		},
	}

	// Test struct template
	var structBuf bytes.Buffer
	err := structTmpl.Execute(&structBuf, s)
	require.NoError(t, err)

	structStr := structBuf.String()
	assert.Contains(t, structStr, "type TestStructLenses struct")
	assert.Contains(t, structStr, "Name L.Lens[TestStruct, string]")
	assert.Contains(t, structStr, "NameO LO.LensO[TestStruct, string]")
	assert.Contains(t, structStr, "Value L.Lens[TestStruct, *int]")
	assert.Contains(t, structStr, "ValueO LO.LensO[TestStruct, *int]")

	// Test constructor template
	var constructorBuf bytes.Buffer
	err = constructorTmpl.Execute(&constructorBuf, s)
	require.NoError(t, err)

	constructorStr := constructorBuf.String()
	assert.Contains(t, constructorStr, "func MakeTestStructLenses() TestStructLenses")
	assert.Contains(t, constructorStr, "return TestStructLenses{")
	assert.Contains(t, constructorStr, "Name: lensName,")
	assert.Contains(t, constructorStr, "NameO: lensNameO,")
	assert.Contains(t, constructorStr, "Value: lensValue,")
	assert.Contains(t, constructorStr, "ValueO: lensValueO,")
	assert.Contains(t, constructorStr, "IO.FromZero")
}

func TestLensTemplatesWithOmitEmpty(t *testing.T) {
	s := structInfo{
		Name: "ConfigStruct",
		Fields: []fieldInfo{
			{Name: "Name", TypeName: "string", IsOptional: false, IsComparable: true},
			{Name: "Value", TypeName: "string", IsOptional: true, IsComparable: true},    // non-pointer with omitempty
			{Name: "Count", TypeName: "int", IsOptional: true, IsComparable: true},       // non-pointer with omitempty
			{Name: "Pointer", TypeName: "*string", IsOptional: true, IsComparable: true}, // pointer
		},
	}

	// Test struct template
	var structBuf bytes.Buffer
	err := structTmpl.Execute(&structBuf, s)
	require.NoError(t, err)

	structStr := structBuf.String()
	assert.Contains(t, structStr, "type ConfigStructLenses struct")
	assert.Contains(t, structStr, "Name L.Lens[ConfigStruct, string]")
	assert.Contains(t, structStr, "NameO LO.LensO[ConfigStruct, string]")
	assert.Contains(t, structStr, "Value L.Lens[ConfigStruct, string]")
	assert.Contains(t, structStr, "ValueO LO.LensO[ConfigStruct, string]", "comparable non-pointer with omitempty should have optional lens")
	assert.Contains(t, structStr, "Count L.Lens[ConfigStruct, int]")
	assert.Contains(t, structStr, "CountO LO.LensO[ConfigStruct, int]", "comparable non-pointer with omitempty should have optional lens")
	assert.Contains(t, structStr, "Pointer L.Lens[ConfigStruct, *string]")
	assert.Contains(t, structStr, "PointerO LO.LensO[ConfigStruct, *string]")

	// Test constructor template
	var constructorBuf bytes.Buffer
	err = constructorTmpl.Execute(&constructorBuf, s)
	require.NoError(t, err)

	constructorStr := constructorBuf.String()
	assert.Contains(t, constructorStr, "func MakeConfigStructLenses() ConfigStructLenses")
	assert.Contains(t, constructorStr, "IO.FromZero[string]()")
	assert.Contains(t, constructorStr, "IO.FromZero[int]()")
	assert.Contains(t, constructorStr, "IO.FromZero[*string]()")
}

func TestLensCommandFlags(t *testing.T) {
	cmd := LensCommand()

	assert.Equal(t, "lens", cmd.Name)
	assert.Equal(t, "generate lens code for annotated structs", cmd.Usage)
	assert.Contains(t, strings.ToLower(cmd.Description), "fp-go:lens")
	assert.Contains(t, strings.ToLower(cmd.Description), "lenso", "Description should mention LensO for optional lenses")

	// Check flags
	assert.Len(t, cmd.Flags, 3)

	var hasDir, hasFilename, hasVerbose bool
	for _, flag := range cmd.Flags {
		switch flag.Names()[0] {
		case "dir":
			hasDir = true
		case "filename":
			hasFilename = true
		case "verbose":
			hasVerbose = true
		}
	}

	assert.True(t, hasDir, "should have dir flag")
	assert.True(t, hasFilename, "should have filename flag")
	assert.True(t, hasVerbose, "should have verbose flag")
}

func TestParseFileWithEmbeddedStruct(t *testing.T) {
	// Create a temporary test file
	tmpDir := t.TempDir()
	testFile := filepath.Join(tmpDir, "test.go")

	testCode := `package testpkg

// Base struct to be embedded
type Base struct {
	ID   int
	Name string
}

// fp-go:Lens
type Extended struct {
	Base
	Extra string
}
`

	err := os.WriteFile(testFile, []byte(testCode), 0644)
	require.NoError(t, err)

	// Parse the file
	structs, pkg, err := parseFile(testFile)
	require.NoError(t, err)

	// Verify results
	assert.Equal(t, "testpkg", pkg)
	assert.Len(t, structs, 1)

	// Check Extended struct
	extended := structs[0]
	assert.Equal(t, "Extended", extended.Name)
	assert.Len(t, extended.Fields, 3, "Should have 3 fields: ID, Name (from Base), and Extra")

	// Check that embedded fields are promoted
	fieldNames := make(map[string]bool)
	for _, field := range extended.Fields {
		fieldNames[field.Name] = true
	}

	assert.True(t, fieldNames["ID"], "Should have promoted ID field from Base")
	assert.True(t, fieldNames["Name"], "Should have promoted Name field from Base")
	assert.True(t, fieldNames["Extra"], "Should have Extra field")
}

func TestGenerateLensHelpersWithEmbeddedStruct(t *testing.T) {
	// Create a temporary directory with test files
	tmpDir := t.TempDir()

	testCode := `package testpkg

// Base struct to be embedded
type Address struct {
	Street string
	City   string
}

// fp-go:Lens
type Person struct {
	Address
	Name string
	Age  int
}
`

	testFile := filepath.Join(tmpDir, "test.go")
	err := os.WriteFile(testFile, []byte(testCode), 0644)
	require.NoError(t, err)

	// Generate lens code
	outputFile := "gen.go"
	err = generateLensHelpers(tmpDir, outputFile, false)
	require.NoError(t, err)

	// Verify the generated file exists
	genPath := filepath.Join(tmpDir, outputFile)
	_, err = os.Stat(genPath)
	require.NoError(t, err)

	// Read and verify the generated content
	content, err := os.ReadFile(genPath)
	require.NoError(t, err)

	contentStr := string(content)

	// Check for expected content
	assert.Contains(t, contentStr, "package testpkg")
	assert.Contains(t, contentStr, "PersonLenses")
	assert.Contains(t, contentStr, "MakePersonLenses")

	// Check that embedded fields are included
	assert.Contains(t, contentStr, "Street L.Lens[Person, string]", "Should have lens for embedded Street field")
	assert.Contains(t, contentStr, "City L.Lens[Person, string]", "Should have lens for embedded City field")
	assert.Contains(t, contentStr, "Name L.Lens[Person, string]", "Should have lens for Name field")
	assert.Contains(t, contentStr, "Age L.Lens[Person, int]", "Should have lens for Age field")

	// Check that optional lenses are also generated for embedded fields
	assert.Contains(t, contentStr, "StreetO LO.LensO[Person, string]")
	assert.Contains(t, contentStr, "CityO LO.LensO[Person, string]")
}

func TestParseFileWithPointerEmbeddedStruct(t *testing.T) {
	// Create a temporary test file
	tmpDir := t.TempDir()
	testFile := filepath.Join(tmpDir, "test.go")

	testCode := `package testpkg

// Base struct to be embedded
type Metadata struct {
	CreatedAt string
	UpdatedAt string
}

// fp-go:Lens
type Document struct {
	*Metadata
	Title   string
	Content string
}
`

	err := os.WriteFile(testFile, []byte(testCode), 0644)
	require.NoError(t, err)

	// Parse the file
	structs, pkg, err := parseFile(testFile)
	require.NoError(t, err)

	// Verify results
	assert.Equal(t, "testpkg", pkg)
	assert.Len(t, structs, 1)

	// Check Document struct
	doc := structs[0]
	assert.Equal(t, "Document", doc.Name)
	assert.Len(t, doc.Fields, 4, "Should have 4 fields: CreatedAt, UpdatedAt (from *Metadata), Title, and Content")

	// Check that embedded fields are promoted
	fieldNames := make(map[string]bool)
	for _, field := range doc.Fields {
		fieldNames[field.Name] = true
	}

	assert.True(t, fieldNames["CreatedAt"], "Should have promoted CreatedAt field from *Metadata")
	assert.True(t, fieldNames["UpdatedAt"], "Should have promoted UpdatedAt field from *Metadata")
	assert.True(t, fieldNames["Title"], "Should have Title field")
	assert.True(t, fieldNames["Content"], "Should have Content field")
}

func TestParseFileWithGenericStruct(t *testing.T) {
	// Create a temporary test file
	tmpDir := t.TempDir()
	testFile := filepath.Join(tmpDir, "test.go")

	testCode := `package testpkg

// fp-go:Lens
type Container[T any] struct {
	Value T
	Count int
}
`

	err := os.WriteFile(testFile, []byte(testCode), 0644)
	require.NoError(t, err)

	// Parse the file
	structs, pkg, err := parseFile(testFile)
	require.NoError(t, err)

	// Verify results
	assert.Equal(t, "testpkg", pkg)
	assert.Len(t, structs, 1)

	// Check Container struct
	container := structs[0]
	assert.Equal(t, "Container", container.Name)
	assert.Equal(t, "[T any]", container.TypeParams, "Should have type parameter [T any]")
	assert.Len(t, container.Fields, 2)

	assert.Equal(t, "Value", container.Fields[0].Name)
	assert.Equal(t, "T", container.Fields[0].TypeName)

	assert.Equal(t, "Count", container.Fields[1].Name)
	assert.Equal(t, "int", container.Fields[1].TypeName)
}

func TestParseFileWithMultipleTypeParams(t *testing.T) {
	// Create a temporary test file
	tmpDir := t.TempDir()
	testFile := filepath.Join(tmpDir, "test.go")

	testCode := `package testpkg

// fp-go:Lens
type Pair[K comparable, V any] struct {
	Key   K
	Value V
}
`

	err := os.WriteFile(testFile, []byte(testCode), 0644)
	require.NoError(t, err)

	// Parse the file
	structs, pkg, err := parseFile(testFile)
	require.NoError(t, err)

	// Verify results
	assert.Equal(t, "testpkg", pkg)
	assert.Len(t, structs, 1)

	// Check Pair struct
	pair := structs[0]
	assert.Equal(t, "Pair", pair.Name)
	assert.Equal(t, "[K comparable, V any]", pair.TypeParams, "Should have type parameters [K comparable, V any]")
	assert.Len(t, pair.Fields, 2)

	assert.Equal(t, "Key", pair.Fields[0].Name)
	assert.Equal(t, "K", pair.Fields[0].TypeName)

	assert.Equal(t, "Value", pair.Fields[1].Name)
	assert.Equal(t, "V", pair.Fields[1].TypeName)
}

func TestGenerateLensHelpersWithGenericStruct(t *testing.T) {
	// Create a temporary directory with test files
	tmpDir := t.TempDir()

	testCode := `package testpkg

// fp-go:Lens
type Box[T any] struct {
	Content T
	Label   string
}
`

	testFile := filepath.Join(tmpDir, "test.go")
	err := os.WriteFile(testFile, []byte(testCode), 0644)
	require.NoError(t, err)

	// Generate lens code
	outputFile := "gen.go"
	err = generateLensHelpers(tmpDir, outputFile, false)
	require.NoError(t, err)

	// Verify the generated file exists
	genPath := filepath.Join(tmpDir, outputFile)
	_, err = os.Stat(genPath)
	require.NoError(t, err)

	// Read and verify the generated content
	content, err := os.ReadFile(genPath)
	require.NoError(t, err)

	contentStr := string(content)

	// Check for expected content with type parameters
	assert.Contains(t, contentStr, "package testpkg")
	assert.Contains(t, contentStr, "type BoxLenses[T any] struct", "Should have generic BoxLenses type")
	assert.Contains(t, contentStr, "type BoxRefLenses[T any] struct", "Should have generic BoxRefLenses type")
	assert.Contains(t, contentStr, "func MakeBoxLenses[T any]() BoxLenses[T]", "Should have generic constructor")
	assert.Contains(t, contentStr, "func MakeBoxRefLenses[T any]() BoxRefLenses[T]", "Should have generic ref constructor")

	// Check that fields use the generic type parameter
	assert.Contains(t, contentStr, "Content L.Lens[Box[T], T]", "Should have lens for generic Content field")
	assert.Contains(t, contentStr, "Label L.Lens[Box[T], string]", "Should have lens for Label field")

	// Check optional lenses - only for comparable types
	// T any is not comparable, so ContentO should NOT be generated
	assert.NotContains(t, contentStr, "ContentO LO.LensO[Box[T], T]", "T any is not comparable, should not have optional lens")
	// string is comparable, so LabelO should be generated
	assert.Contains(t, contentStr, "LabelO LO.LensO[Box[T], string]", "string is comparable, should have optional lens")
}

func TestGenerateLensHelpersWithComparableTypeParam(t *testing.T) {
	// Create a temporary directory with test files
	tmpDir := t.TempDir()

	testCode := `package testpkg

// fp-go:Lens
type ComparableBox[T comparable] struct {
	Key   T
	Value string
}
`

	testFile := filepath.Join(tmpDir, "test.go")
	err := os.WriteFile(testFile, []byte(testCode), 0644)
	require.NoError(t, err)

	// Generate lens code
	outputFile := "gen.go"
	err = generateLensHelpers(tmpDir, outputFile, false)
	require.NoError(t, err)

	// Verify the generated file exists
	genPath := filepath.Join(tmpDir, outputFile)
	_, err = os.Stat(genPath)
	require.NoError(t, err)

	// Read and verify the generated content
	content, err := os.ReadFile(genPath)
	require.NoError(t, err)

	contentStr := string(content)

	// Check for expected content with type parameters
	assert.Contains(t, contentStr, "package testpkg")
	assert.Contains(t, contentStr, "type ComparableBoxLenses[T comparable] struct", "Should have generic ComparableBoxLenses type")
	assert.Contains(t, contentStr, "type ComparableBoxRefLenses[T comparable] struct", "Should have generic ComparableBoxRefLenses type")

	// Check that Key field (with comparable constraint) uses MakeLensStrict in RefLenses
	assert.Contains(t, contentStr, "lensKey := L.MakeLensStrict(", "Key field with comparable constraint should use MakeLensStrict")

	// Check that Value field (string, always comparable) also uses MakeLensStrict
	assert.Contains(t, contentStr, "lensValue := L.MakeLensStrict(", "Value field (string) should use MakeLensStrict")

	// Verify that MakeLensRef is NOT used (since both fields are comparable)
	assert.NotContains(t, contentStr, "L.MakeLensRef(", "Should not use MakeLensRef when all fields are comparable")
}
